import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

public class AES128Core {

	private short[][] data;//data array
	private static final byte DATA_ARRAY_ROWS = 4;
	private static final byte DATA_ARRAY_COLS = 4;

	//sbox
	private static short[][] sbox= {
			{0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76}, /*0*/ 
			{0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0}, /*1*/
			{0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15}, /*2*/
			{0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75}, /*3*/
			{0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84}, /*4*/
			{0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf}, /*5*/
			{0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8}, /*6*/ 
			{0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2}, /*7*/
			{0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73}, /*8*/
			{0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb}, /*9*/
			{0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79}, /*a*/
			{0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08}, /*b*/
			{0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a}, /*c*/
			{0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e}, /*d*/
			{0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf}, /*e*/
			{0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16}  /*f*/
	};
	//inverse sbox
	private static short[][] inverseSBox = {
			{0x52,0x09,0x6a,0xd5,0x30,0x36,0xa5,0x38,0xbf,0x40,0xa3,0x9e,0x81,0xf3,0xd7,0xfb}, /*0*/
			{0x7c,0xe3,0x39,0x82,0x9b,0x2f,0xff,0x87,0x34,0x8e,0x43,0x44,0xc4,0xde,0xe9,0xcb}, /*1*/
			{0x54,0x7b,0x94,0x32,0xa6,0xc2,0x23,0x3d,0xee,0x4c,0x95,0x0b,0x42,0xfa,0xc3,0x4e}, /*2*/
			{0x08,0x2e,0xa1,0x66,0x28,0xd9,0x24,0xb2,0x76,0x5b,0xa2,0x49,0x6d,0x8b,0xd1,0x25}, /*3*/
			{0x72,0xf8,0xf6,0x64,0x86,0x68,0x98,0x16,0xd4,0xa4,0x5c,0xcc,0x5d,0x65,0xb6,0x92}, /*4*/
			{0x6c,0x70,0x48,0x50,0xfd,0xed,0xb9,0xda,0x5e,0x15,0x46,0x57,0xa7,0x8d,0x9d,0x84}, /*5*/
			{0x90,0xd8,0xab,0x00,0x8c,0xbc,0xd3,0x0a,0xf7,0xe4,0x58,0x05,0xb8,0xb3,0x45,0x06}, /*6*/
			{0xd0,0x2c,0x1e,0x8f,0xca,0x3f,0x0f,0x02,0xc1,0xaf,0xbd,0x03,0x01,0x13,0x8a,0x6b}, /*7*/
			{0x3a,0x91,0x11,0x41,0x4f,0x67,0xdc,0xea,0x97,0xf2,0xcf,0xce,0xf0,0xb4,0xe6,0x73}, /*8*/
			{0x96,0xac,0x74,0x22,0xe7,0xad,0x35,0x85,0xe2,0xf9,0x37,0xe8,0x1c,0x75,0xdf,0x6e}, /*9*/
			{0x47,0xf1,0x1a,0x71,0x1d,0x29,0xc5,0x89,0x6f,0xb7,0x62,0x0e,0xaa,0x18,0xbe,0x1b}, /*a*/
			{0xfc,0x56,0x3e,0x4b,0xc6,0xd2,0x79,0x20,0x9a,0xdb,0xc0,0xfe,0x78,0xcd,0x5a,0xf4}, /*b*/
			{0x1f,0xdd,0xa8,0x33,0x88,0x07,0xc7,0x31,0xb1,0x12,0x10,0x59,0x27,0x80,0xec,0x5f}, /*c*/
			{0x60,0x51,0x7f,0xa9,0x19,0xb5,0x4a,0x0d,0x2d,0xe5,0x7a,0x9f,0x93,0xc9,0x9c,0xef}, /*d*/
			{0xa0,0xe0,0x3b,0x4d,0xae,0x2a,0xf5,0xb0,0xc8,0xeb,0xbb,0x3c,0x83,0x53,0x99,0x61}, /*e*/
			{0x17,0x2b,0x04,0x7e,0xba,0x77,0xd6,0x26,0xe1,0x69,0x14,0x63,0x55,0x21,0x0c,0x7d}  /*f*/
	};
	//fixed matrix
	private static final short[][] fixedMat = {
			{0x02, 0x03, 0x01, 0x01},
			{0x01, 0x02, 0x03, 0x01},
			{0x01, 0x01, 0x02, 0x03},
			{0x03, 0x01, 0x01, 0x02}
	};
	private static final short[][] inverseFixedMat = {
			{0x0e, 0x0b, 0x0d, 0x09},
			{0x09, 0x0e, 0x0b, 0x0d},
			{0x0d, 0x09, 0x0e, 0x0b},
			{0x0b, 0x0d, 0x09, 0x0e}
	};

	private short[][] key;//key matrix
	private static final byte KEY_ARRAY_ROWS = 4;
	private static final byte KEY_ARRAY_COLS = 44;
	private static final byte KEY_ARRAY_PRIMARY_COLS = 4;

	//round
	private static final byte ROUND_MEDIUM = 9;
	//rcon constant array
	private static final short[][] rcon= {
			{0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1b,0x36},
			{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
			{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
			{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}
	};

	//plain polynomial
	private static final short plainPolynomial = 0x1b;

	//mode
	private String mode = "";
	//IV vector, for mode except ECB mode
	private short[][] IV;
	//encryption matrix, for next sector encryption/decryption
	private short[][] encryptionMat;
	
	//counter constructor, only for CTR mode
	private long nonce = 0;

	/**
	 * @author baiwenxin
	 * @param short[] key, key provided
	 * @return None
	 * @brief Constructor for AES128Core
	 */
	public AES128Core(short[] key) {
		// TODO Auto-generated constructor stub
		if(KEY_ARRAY_PRIMARY_COLS * KEY_ARRAY_ROWS != key.length) {
			System.out.println("key length not equal to "+Integer.toString(KEY_ARRAY_COLS * KEY_ARRAY_ROWS)+"\n");
		}

		this.key = new short[KEY_ARRAY_ROWS][KEY_ARRAY_COLS];
		for(int col = 0; col < KEY_ARRAY_PRIMARY_COLS; col++) {
			for(int row = 0; row < KEY_ARRAY_ROWS; row++) {
				this.key[row][col] = key[col * KEY_ARRAY_ROWS + row]; 
			}
		}
		
		this.data = new short[DATA_ARRAY_ROWS][DATA_ARRAY_COLS];
		
		//first generate all keys
		keyExpansion();
	}

	/**
	 * @author baiwenxin
	 * @param String mode, mode selected
	 * @param short[] IV, IV vector provided
	 * @param short[] key, key provided
	 * @return None
	 * @brief Constructor for AES128Core
	 */
	public AES128Core(String mode, short[] key, short[] IV) {
		if((!mode.equals("CBC")) && (!mode.equals("CFB")) && (!mode.equals("OFB")) && (!mode.equals("CTR")) && (!mode.equals("PCBC"))) {
			System.out.print("mode isn't supported\n");
		}

		this.mode = mode;
		// TODO Auto-generated constructor stub
		if(KEY_ARRAY_PRIMARY_COLS * KEY_ARRAY_ROWS != key.length) {
			System.out.println("key length not equal to "+Integer.toString(KEY_ARRAY_COLS * KEY_ARRAY_ROWS)+"\n");
		}

		this.key = new short[KEY_ARRAY_ROWS][KEY_ARRAY_COLS];
		for(int col = 0; col < KEY_ARRAY_PRIMARY_COLS; col++) {
			for(int row = 0; row < KEY_ARRAY_ROWS; row++) {
				this.key[row][col] = key[col * KEY_ARRAY_ROWS + row]; 
			}
		}
		
		this.data = new short[DATA_ARRAY_ROWS][DATA_ARRAY_COLS];
		
		//first generate all keys
		keyExpansion();

		this.IV = new short[DATA_ARRAY_ROWS][DATA_ARRAY_COLS];
		for(int row = 0; row < DATA_ARRAY_ROWS; row++) {
			for(int col = 0; col < DATA_ARRAY_COLS; col++) {
				this.IV[row][col] = IV[col * DATA_ARRAY_ROWS + row];
			}
		}
		
		this.encryptionMat = new short[DATA_ARRAY_ROWS][DATA_ARRAY_COLS];
	}
	
	/**
	 * @author baiwenxin
	 * @param mode, only "CTR" accepted
	 * @param nonce, CTR mode counter prefix
	 * @return None
	 */
	public AES128Core(String mode, long nonce, short[] key) {
		if(!mode.equals("CTR")) {
			System.out.print("mode is not suppprted.\n");
		}
		this.mode = mode;
		this.nonce = nonce;
		this.data = new short[DATA_ARRAY_ROWS][DATA_ARRAY_COLS];
		if(KEY_ARRAY_PRIMARY_COLS * KEY_ARRAY_ROWS != key.length) {
			System.out.println("key length not equal to "+Integer.toString(KEY_ARRAY_COLS * KEY_ARRAY_ROWS)+"\n");
		}

		this.key = new short[KEY_ARRAY_ROWS][KEY_ARRAY_COLS];
		for(int col = 0; col < KEY_ARRAY_PRIMARY_COLS; col++) {
			for(int row = 0; row < KEY_ARRAY_ROWS; row++) {
				this.key[row][col] = key[col * KEY_ARRAY_ROWS + row]; 
			}
		}
		
		keyExpansion();
	}

	/**
	 * @author baiwenxin
	 * @return None
	 * @param data, new data to be updated to data matrix
	 * @brief update data, i.e. next 16 bytes array
	 */
	public void setDataMatrix(short[] data) {
		for(int col = 0; col < DATA_ARRAY_COLS; col++) {
			for(int row = 0; row < DATA_ARRAY_ROWS; row++) {
				this.data[row][col] = data[col * DATA_ARRAY_COLS + row]; 
			}
		}
	}
	
	/**
	 * @author baiwenxin
	 * @return None
	 * @param data, new data to be updated to data matrix
	 * @brief update data, i.e. next 16 bytes array
	 */
	public void setDataMatrix(short[][] data) {
		for(int col = 0; col < DATA_ARRAY_COLS; col++) {
			for(int row = 0; row < DATA_ARRAY_ROWS; row++) {
				this.data[row][col] = data[row][col]; 
			}
		}
	}
	
	/**
	 * @author baiwenxin
	 * @param None
	 * @brief give value to encryption matrix (copy data matrix to encryption matrix)
	 * @return None
	 */
	public void setEncryptionMatrix() {
		for(int row = 0; row < DATA_ARRAY_ROWS; row++) {
			for(int col = 0; col < DATA_ARRAY_COLS; col++) {
				this.encryptionMat[row][col] = this.data[row][col];
			}
		}
	}
	
	/**
	 * @author baiwenxin
	 * @param None
	 * @brief give value to encryption matrix (copy plainText to encryption matrix)
	 * @return None
	 */
	public void setEncryptionMatrix(byte[] plainText) {
		for(int row = 0; row < DATA_ARRAY_ROWS; row++) {
			for(int col = 0; col < DATA_ARRAY_COLS; col++) {
				this.encryptionMat[row][col] = plainText[col * DATA_ARRAY_ROWS + row];
			}
		}
	}
	
	/**
	 * @author baiwenxin
	 * @param None
	 * @return None
	 * @brief xor operation of data matrix and encryption matrix
	 */
	private void XOREncryptionMat() {
		for(int row = 0; row < DATA_ARRAY_ROWS; row++) {
			for(int col = 0; col < DATA_ARRAY_COLS; col++) {
				this.data[row][col] = (short)(this.data[row][col] ^ this.encryptionMat[row][col] & 0x00ff);
			}
		}
	}

	/**
	 * @author baiwenxin
	 * @return None
	 * @param data, new data to be updated to data matrix
	 * @brief update data, i.e. next 16 bytes array
	 */
	public void setDataMatrix(byte[] data) {
		for(int col = 0; col < DATA_ARRAY_COLS; col++) {
			for(int row = 0; row < DATA_ARRAY_ROWS; row++) {
				this.data[row][col] = data[col * DATA_ARRAY_COLS + row]; 
			}
		}
	}

	/**
	 * @author baiwenxin
	 * @return byte[], data to be written
	 * @param None
	 * @brief get data matrix (one-dimensional)
	 */
	public byte[] getDataMatrix() {
		byte[] buffer = new byte[DATA_ARRAY_ROWS*DATA_ARRAY_COLS];
		for(int col = 0; col < DATA_ARRAY_COLS; col++) {
			for(int row = 0; row < DATA_ARRAY_ROWS; row++) {
				buffer[col * DATA_ARRAY_COLS + row] = (byte)this.data[row][col]; 
			}
		}
		return buffer;
	}

	/**
	 * @author baiwenxin
	 * @return None
	 * @param None
	 * @brief print current data array, mainly for debug
	 */
	public void printCurrentDataArray() {
		System.out.print("current data array:\n");
		for(int row = 0; row < DATA_ARRAY_ROWS; row++) {
			for(int col = 0; col < DATA_ARRAY_COLS; col++) {
				System.out.print(Integer.toHexString(this.data[row][col]) + " ");
			}
			System.out.print("\n");
		}
		System.out.print("\n");
		System.out.print("current key array:\n");
		for(int row = 0; row < KEY_ARRAY_ROWS; row++) {
			for(int col = 0; col < KEY_ARRAY_COLS; col++) {
				System.out.print(Integer.toHexString(this.key[row][col]) + " ");
			}
			System.out.print("\n");
		}
		System.out.print("\n");
	}

	/**
	 * @author baiwenxin
	 * @param None
	 * @return None
	 * @brief encrypt data array - main function
	 */
	public void encrypt(){
			addRoundKey(-1);
			for(int round = 0; round < ROUND_MEDIUM; round++) {
				subBytes(true);
				for(int i = 1; i < DATA_ARRAY_ROWS; i++) {
					cycleMoveElement(i, i, true);
				}
				mixColumns(true);
				addRoundKey(round);
			}
			subBytes(true);
			for(int i = 1; i < DATA_ARRAY_ROWS; i++) {
				cycleMoveElement(i, i, true);
			}
			addRoundKey(9);
	}

	/**
	 * @author baiwenxin
	 * @param None
	 * @return None
	 * @brief xor operation with IV vector
	 */
	private void XORIV() {
		for(int row = 0; row < DATA_ARRAY_ROWS; row++) {
			for(int col = 0; col < DATA_ARRAY_COLS; col++) {
				this.data[row][col] ^= this.IV[row][col];
			}
		}
	}
	
	/**
	 * @author baiwenxin
	 * @return None
	 * @param plainText
	 * @brief data matrix XOR plain text matrix
	 */
	private void XORPlainText(short[] plainText) {
		for(int row = 0; row < DATA_ARRAY_ROWS; row++) {
			for(int col = 0; col < DATA_ARRAY_COLS; col++) {
				this.data[row][col] ^= plainText[col * DATA_ARRAY_ROWS + row];
			}
		}
	}
	
	/**
	 * @author baiwenxin
	 * @return None
	 * @param plainText
	 * @brief data matrix XOR plain text matrix
	 */
	private void XORPlainText(byte[] plainText) {
		for(int row = 0; row < DATA_ARRAY_ROWS; row++) {
			for(int col = 0; col < DATA_ARRAY_COLS; col++) {
				this.data[row][col] ^= plainText[col * DATA_ARRAY_ROWS + row];
			}
		}
	}

	/**
	 * @author baiwenxin
	 * @param None
	 * @return None
	 * @brief decrypt encrypted array
	 */
	public void decrypt() {
		
			addRoundKey(9);
			for(int round = 0; round < ROUND_MEDIUM; round++) {

				for(int i = 1; i < DATA_ARRAY_ROWS; i++) {
					cycleMoveElement(i, i, false);
				}
				subBytes(false);
				addRoundKey(8-round);

				mixColumns(false);

			}
			for(int i = 1; i < DATA_ARRAY_ROWS; i++) {
				cycleMoveElement(i, i, false);
			}
			subBytes(false);
			addRoundKey(-1);
	}

	/**
	 * @author baiwenxin
	 * @param boolean isEncode, true:encode, false:decode
	 * @return None
	 * @brief substitute bytes in state matrix
	 */
	private void subBytes(boolean isEncode) {
		if(true == isEncode) {
			for(int row = 0; row < DATA_ARRAY_ROWS; row++) {
				for(int col = 0; col < DATA_ARRAY_COLS; col++) {
					short stateValue = this.data[row][col];
					int colSBox = stateValue&0x000f;
					int rowSBox = stateValue>>4;
				this.data[row][col] = sbox[rowSBox][colSBox];
				}
			}
		}else{
			for(int row = 0; row < DATA_ARRAY_ROWS; row++) {
				for(int col = 0; col < DATA_ARRAY_COLS; col++) {
					short stateValue = this.data[row][col];
					int colSBox = stateValue&0x000f;
					int rowSBox = stateValue>>4;
				this.data[row][col] = inverseSBox[rowSBox][colSBox];
				}
			}
		}
	}


	/**
	 * @author baiwenxin
	 * @param rowIndex, cycle move which row of state matrix
	 * @param cycles, cycle numbers
	 * @param boolean isEncode, indicating whether the process is encode
	 * @return None
	 * @brief cycle move(toward left) one row with cycle numbers
	 */
	private void cycleMoveElement(int rowIndex, int cycles, boolean isEncode) {
		if(isEncode) {
			if(1 == cycles) {
				short firstElement = this.data[rowIndex][0];
				for(int i = 0; i < DATA_ARRAY_COLS - 1; i++) {
					this.data[rowIndex][i] = this.data[rowIndex][i+1];
				}
				this.data[rowIndex][DATA_ARRAY_COLS-1] = firstElement;
			}else {
				for(int i = 0; i < cycles; i++) {
					cycleMoveElement(rowIndex, 1, isEncode);
				}
			}
		}else {
			if(1 == cycles) {
				short firstElement = this.data[rowIndex][DATA_ARRAY_COLS - 1];
				for(int i = DATA_ARRAY_COLS - 1; i > 0; i--) {
					this.data[rowIndex][i] = this.data[rowIndex][i-1];
				}
				this.data[rowIndex][0] = firstElement;
			}else {
				for(int i = 0; i < cycles; i++) {
					cycleMoveElement(rowIndex, 1, isEncode);
				}
			}
		}

	}

	/**
	 * @author baiwenxin
	 * @param int round, round number
	 * @return None
	 * @brief add round key, i.e. add state matrix and key matrix (Galois Field add)
	 */
	private void addRoundKey(int round) {
		for(int row = 0; row < DATA_ARRAY_ROWS; row++) {
			for(int col = 0; col < DATA_ARRAY_COLS; col++) {
				short valueState = this.data[row][col];
				valueState ^= this.key[row][(round+1) * KEY_ARRAY_PRIMARY_COLS + col];
				valueState &= 0x00ff;
				this.data[row][col] = valueState;
			}
		}
	}

	/**
	 * @author baiwenxin
	 * @param boolean isEncode, indicating whether the process is encode
	 * @return None
	 * @brief mix columns in state matrix, i.e. fixed matrix (GF*) state matrix
	 */
	private void mixColumns(boolean isEncode) {
		if(isEncode) {
			short[][] multiplicationValMat = new short[DATA_ARRAY_ROWS][DATA_ARRAY_COLS];
			for(int row = 0; row < DATA_ARRAY_ROWS; row++) {
				for(int col = 0; col < DATA_ARRAY_COLS; col++) {
					short sum = 0;
					for(int sumIndex = 0; sumIndex < DATA_ARRAY_COLS; sumIndex++) {
						short num = multiply(this.data[sumIndex][col], fixedMat[row][sumIndex]);
						sum = (short)((sum ^ num)&0x00ff);
					}
					multiplicationValMat[row][col] = sum;
				}
			}
			//clone data
			for(int row = 0; row < DATA_ARRAY_ROWS; row++) {
				for(int col = 0; col < DATA_ARRAY_COLS; col++) {
					this.data[row][col] = multiplicationValMat[row][col];
				}
			}
		}else {
			short[][] multiplicationValMat = new short[DATA_ARRAY_ROWS][DATA_ARRAY_COLS];
			for(int row = 0; row < DATA_ARRAY_ROWS; row++) {
				for(int col = 0; col < DATA_ARRAY_COLS; col++) {
					short sum = 0;
					for(int sumIndex = 0; sumIndex < DATA_ARRAY_COLS; sumIndex++) {
						short num = multiply(this.data[sumIndex][col], inverseFixedMat[row][sumIndex]);
						sum = (short)((sum ^ num)&0x00ff);
					}
					multiplicationValMat[row][col] = sum;
				}
			}
			//clone data
			for(int row = 0; row < DATA_ARRAY_ROWS; row++) {
				for(int col = 0; col < DATA_ARRAY_COLS; col++) {
					this.data[row][col] = multiplicationValMat[row][col];
				}
			}
		}

	}

	/**
	 * @author baiwenxin
	 * @param ori, value to be multiplied
	 * @param fixedVal, in this example, 1 or 2 or 3 (in the fixed matrix)
	 * @return short, result of multiplication
	 * @brief multiple two data in GF Field
	 */
	public short multiply(short ori, int fixedVal) {
		//split fixedVal
		short symbol = 0x01;
		short num = 0;
		for(int i = 0; i < 8; i++){
			if((fixedVal & (symbol << i)) == (symbol << i)){
				//a need to left move i bits
				short temp = ori;
				for(int j = 0; j < i; j++){
					temp = (short)(((temp & 0x80) == 0x80) ? ((temp << 1 & 0x00ff) ^ plainPolynomial) : (temp << 1 & 0x00ff));
				}
				num ^= temp;
			}
		}
		return  num;
	}

	/**
	 * @author baiwenxin
	 * @param int round, indicating the round number, 0-8
	 * @return None
	 * @brief key schedule, i.e. update key matrix
	 */
	private void keyExpansion() {
		for(int col = 4; col < KEY_ARRAY_COLS; col++) {
			if(0 == col % 4) {
				for(int row = 0; row < KEY_ARRAY_ROWS; row++) {
					//w[n-4]
					short wn_4 = this.key[row][col - 4];
					//w[n-1] after rot: (row+1)%4==RotWord
					short wn_1 = this.key[(row+1)%4][col - 1];
					//subword
					int colSBox = wn_1&0x000f;
					int rowSBox = wn_1>>4;
				short subbyteVal = sbox[rowSBox][colSBox];
				short firstXORVal = (short)(wn_4 ^ subbyteVal & 0x00ff);
				short rconVal = rcon[row][col / 4 - 1];
				this.key[row][col] = (short)(firstXORVal ^ rconVal & 0x00ff);
				}
			}else {
				for(int row = 0; row < KEY_ARRAY_ROWS; row++) {
					//w[n-4]^w[n-1]
					this.key[row][col] = (short)(this.key[row][col - 4] ^ this.key[row][col - 1] & 0x00ff);
				}
			}
		}
	}
	
	/**
	 * @author baiwenxin
	 * @param long counterSuffix, counter suffix self-incremented in each round
	 * @return byte[], counter in CTR mode
	 */
	private byte[] getCounter(long counterSuffix) {
		byte[] counter = new byte[16];
		for(int i = 0; i < 8; i++) {
			counter[i] = (byte)((this.nonce & (0xff << (i*8))) >> (i*8));
		}
		for(int i = 8; i < 16; i++) {
			counter[i] = (byte)((counterSuffix & (0xff << (i*8))) >> (i*8));
		}
		return counter;
	}

	/**
	 * @author baiwenxin
	 * @param inputFilePath, path of input file
	 * @param outputFilePath, path of output file
	 * @brief encrypt file
	 * @return None
	 */
	public void encryptFile(String inputFilePath, String outputFilePath) {
		if(this.mode.equals("")) {
			System.out.print("(mode ECB)encrypt start...\n");
			byte[] buffer = new byte[16];
			int count = 0;
			File fileInput = new File(inputFilePath);
			long inputLen = fileInput.length();
			File fileOutput = new File(outputFilePath);
			try {
				InputStream inputStream = new FileInputStream(fileInput);
				OutputStream outputStream = new FileOutputStream(fileOutput);
				byte[] lengthSign = new byte[8];
				for(int i = 0; i < 8; i++) {
					lengthSign[i] = (byte)(((0xff << (i*8)) & inputLen) >> (i*8)); 
				}
				outputStream.write(lengthSign);
				while(-1!=(count=inputStream.read(buffer))) {
					if(count != 16) {//0 padding
						for(int i = count; i < 16; i++) {
							buffer[i] = 0x00;
						}
					}
					setDataMatrix(buffer);
					encrypt();
					byte[] dataToWrite = getDataMatrix();
					outputStream.write(dataToWrite);
				}
				inputStream.close();
				outputStream.close();
				System.out.print("encrypt end...\n");
			}catch(IOException e) {
				e.printStackTrace();
			}
		}else if(this.mode.equals("CBC")) {
			System.out.print("(mode CBC)encrypt start...\n");
			byte[] buffer = new byte[16];
			int count = 0;
			File fileInput = new File(inputFilePath);
			long inputLen = fileInput.length();
			File fileOutput = new File(outputFilePath);
			try {
				InputStream inputStream = new FileInputStream(fileInput);
				OutputStream outputStream = new FileOutputStream(fileOutput);
				byte[] lengthSign = new byte[8];
				for(int i = 0; i < 8; i++) {
					lengthSign[i] = (byte)(((0xff << (i*8)) & inputLen) >> (i*8)); 
				}
				outputStream.write(lengthSign);
				boolean isFirst16byte = true;
				while(-1!=(count=inputStream.read(buffer))) {
					if(count != 16) {//0 padding
						for(int i = count; i < 16; i++) {
							buffer[i] = 0x00;
						}
					}
					setDataMatrix(buffer);
					
					//encrypt
					if(isFirst16byte) {
						XORIV();
						isFirst16byte = false;
					}else {
						XOREncryptionMat();
					}
					encrypt();
					setEncryptionMatrix();
					
					byte[] dataToWrite = getDataMatrix();
					outputStream.write(dataToWrite);
				}
				inputStream.close();
				outputStream.close();
				System.out.print("encrypt end...\n");
			}catch(IOException e) {
				e.printStackTrace();
			}
		}else if(this.mode.equals("CFB")) {
			System.out.print("(mode CFB)encrypt start...\n");
			byte[] buffer = new byte[16];
			int count = 0;
			File fileInput = new File(inputFilePath);
			long inputLen = fileInput.length();
			File fileOutput = new File(outputFilePath);
			try {
				InputStream inputStream = new FileInputStream(fileInput);
				OutputStream outputStream = new FileOutputStream(fileOutput);
				byte[] lengthSign = new byte[8];
				for(int i = 0; i < 8; i++) {
					lengthSign[i] = (byte)(((0xff << (i*8)) & inputLen) >> (i*8)); 
				}
				outputStream.write(lengthSign);
				boolean isFirst16byte = true;
				while(-1!=(count=inputStream.read(buffer))) {
					if(count != 16) {//0 padding
						for(int i = count; i < 16; i++) {
							buffer[i] = 0x00;
						}
					}
					//plain text in buffer
					//encrypt
					if(isFirst16byte) {
						setDataMatrix(this.IV);
						isFirst16byte = false;
					}else {
						setDataMatrix(this.encryptionMat);
					}
					encrypt();
					XORPlainText(buffer);
					setEncryptionMatrix();
					
					byte[] dataToWrite = getDataMatrix();
					outputStream.write(dataToWrite);
				}
				inputStream.close();
				outputStream.close();
				System.out.print("encrypt end...\n");
			}catch(IOException e) {
				e.printStackTrace();
			}
		}else if(this.mode.equals("OFB")) {
			System.out.print("(mode OFB)encrypt start...\n");
			byte[] buffer = new byte[16];
			int count = 0;
			File fileInput = new File(inputFilePath);
			long inputLen = fileInput.length();
			File fileOutput = new File(outputFilePath);
			try {
				InputStream inputStream = new FileInputStream(fileInput);
				OutputStream outputStream = new FileOutputStream(fileOutput);
				byte[] lengthSign = new byte[8];
				for(int i = 0; i < 8; i++) {
					lengthSign[i] = (byte)(((0xff << (i*8)) & inputLen) >> (i*8)); 
				}
				outputStream.write(lengthSign);
				boolean isFirst16byte = true;
				while(-1!=(count=inputStream.read(buffer))) {
					if(count != 16) {//0 padding
						for(int i = count; i < 16; i++) {
							buffer[i] = 0x00;
						}
					}
					//plain text in buffer
					//encrypt
					if(isFirst16byte) {
						setDataMatrix(this.IV);
						isFirst16byte = false;
					}else {
						setDataMatrix(this.encryptionMat);
					}
					encrypt();
					setEncryptionMatrix();
					XORPlainText(buffer);
					
					
					byte[] dataToWrite = getDataMatrix();
					outputStream.write(dataToWrite);
				}
				inputStream.close();
				outputStream.close();
				System.out.print("encrypt end...\n");
			}catch(IOException e) {
				e.printStackTrace();
			}
		}else if(this.mode.equals("PCBC")) {
			System.out.print("(mode PCBC)encrypt start...\n");
			byte[] buffer = new byte[16];
			int count = 0;
			File fileInput = new File(inputFilePath);
			long inputLen = fileInput.length();
			File fileOutput = new File(outputFilePath);
			try {
				InputStream inputStream = new FileInputStream(fileInput);
				OutputStream outputStream = new FileOutputStream(fileOutput);
				byte[] lengthSign = new byte[8];
				for(int i = 0; i < 8; i++) {
					lengthSign[i] = (byte)(((0xff << (i*8)) & inputLen) >> (i*8)); 
				}
				outputStream.write(lengthSign);
				boolean isFirst16byte = true;
				while(-1!=(count=inputStream.read(buffer))) {
					if(count != 16) {//0 padding
						for(int i = count; i < 16; i++) {
							buffer[i] = 0x00;
						}
					}
					//plain text in buffer
					setDataMatrix(buffer);
					//encrypt
					if(isFirst16byte) {
						XORIV();
						isFirst16byte = false;
					}else {
						XOREncryptionMat();
					}
					encrypt();		
					byte[] dataToWrite = getDataMatrix();
					outputStream.write(dataToWrite);
					XORPlainText(buffer);
					setEncryptionMatrix();
				}
				inputStream.close();
				outputStream.close();
				System.out.print("encrypt end...\n");
			}catch(IOException e) {
				e.printStackTrace();
			}
		}else if(this.mode.equals("CTR")) {
			System.out.print("(mode CTR)encrypt start...\n");
			byte[] buffer = new byte[16];
			int count = 0;
			File fileInput = new File(inputFilePath);
			long inputLen = fileInput.length();
			File fileOutput = new File(outputFilePath);
			try {
				InputStream inputStream = new FileInputStream(fileInput);
				OutputStream outputStream = new FileOutputStream(fileOutput);
				byte[] lengthSign = new byte[8];
				for(int i = 0; i < 8; i++) {
					lengthSign[i] = (byte)(((0xff << (i*8)) & inputLen) >> (i*8)); 
				}
				outputStream.write(lengthSign);
				int counterSuffix = 0;
				while(-1!=(count=inputStream.read(buffer))) {
					if(count != 16) {//0 padding
						for(int i = count; i < 16; i++) {
							buffer[i] = 0x00;
						}
					}
					byte[] counter = getCounter(counterSuffix);
					//plain text in buffer
					setDataMatrix(counter);
					encrypt();
					XORPlainText(buffer);
					byte[] dataToWrite = getDataMatrix();
					outputStream.write(dataToWrite);
					counterSuffix++;
				}
				inputStream.close();
				outputStream.close();
				System.out.print("encrypt end...\n");
			}catch(IOException e) {
				e.printStackTrace();
			}
		}
		
	}

	/**
	 * @author baiwenxin
	 * @param str, string to be encrypted
	 * @return String, encrypted string
	 * @brief encrypt string
	 */
	public String encryptString(String str) {
		String output = "";
		return output;
	}

	/**
	 * @author baiwenxin
	 * @param inputFilePath, path of input file
	 * @param outputFilePath, path of output file
	 * @brief decrypt file
	 */
	public void decryptFile(String inputFilePath, String outputFilePath) {
		
		if(this.mode.equals("")) {
			//decryption--------------------------
			System.out.print("(mode ECB)decrypt start...\n");
			File fileInput = new File(inputFilePath);
			File fileOutput = new File(outputFilePath);
			int count = 0;
			byte[] buffer = new byte[16];
			try {
				InputStream inputStream = new FileInputStream(fileInput);
				byte[] lengthSign = new byte[8];
				inputStream.read(lengthSign);
				long fileLen = 0;
				for(int i = 0; i < 8; i++) {
					fileLen += (long)(lengthSign[i] << (i*8));
				}
				OutputStream outputStream = new FileOutputStream(fileOutput);
				while(-1!=(count=inputStream.read(buffer))) {
					setDataMatrix(buffer);
					decrypt();
					byte[] dataToWrite = getDataMatrix();
					if(fileLen >= 16) {
						outputStream.write(dataToWrite);
						fileLen -= 16;
					}else {
						outputStream.write(dataToWrite, 0, (int)fileLen);
					}
				}
				inputStream.close();
				outputStream.close();
				System.out.print("decrypt end...\n");
			}catch(IOException e) {
				e.printStackTrace();
			}
		}else if(this.mode.equals("CBC")) {
			//decryption--------------------------
			System.out.print("(mode CBC)decrypt start...\n");
			File fileInput = new File(inputFilePath);
			File fileOutput = new File(outputFilePath);
			int count = 0;
			byte[] buffer = new byte[16];
			try {
				InputStream inputStream = new FileInputStream(fileInput);
				byte[] lengthSign = new byte[8];
				inputStream.read(lengthSign);
				long fileLen = 0;
				for(int i = 0; i < 8; i++) {
					fileLen += (long)(lengthSign[i] << (i*8));
				}
				OutputStream outputStream = new FileOutputStream(fileOutput);
				boolean isFirst16byte = true;
				while(-1!=(count=inputStream.read(buffer))) {
					setDataMatrix(buffer);
					decrypt();
					if(isFirst16byte) {
						XORIV();
						isFirst16byte = false;
					}else {
						XOREncryptionMat();
					}
					setEncryptionMatrix(buffer);
					byte[] dataToWrite = getDataMatrix();
					if(fileLen >= 16) {
						outputStream.write(dataToWrite);
						fileLen -= 16;
					}else {
						outputStream.write(dataToWrite, 0, (int)fileLen);
					}
				}
				inputStream.close();
				outputStream.close();
				System.out.print("decrypt end...\n");
			}catch(IOException e) {
				e.printStackTrace();
			}
		}else if(this.mode.equals("CFB")) {
			//decryption--------------------------
			System.out.print("(mode CFB)decrypt start...\n");
			File fileInput = new File(inputFilePath);
			File fileOutput = new File(outputFilePath);
			int count = 0;
			byte[] buffer = new byte[16];
			try {
				InputStream inputStream = new FileInputStream(fileInput);
				byte[] lengthSign = new byte[8];
				inputStream.read(lengthSign);
				long fileLen = 0;
				for(int i = 0; i < 8; i++) {
					fileLen += (long)(lengthSign[i] << (i*8));
				}
				OutputStream outputStream = new FileOutputStream(fileOutput);
				boolean isFirst16byte = true;
				while(-1!=(count=inputStream.read(buffer))) {
					if(isFirst16byte) {
						setDataMatrix(this.IV);
						isFirst16byte = false;
					}else {
						setDataMatrix(this.encryptionMat);
					}
					encrypt();
					XORPlainText(buffer);
					setEncryptionMatrix(buffer);
					byte[] dataToWrite = getDataMatrix();
					if(fileLen >= 16) {
						outputStream.write(dataToWrite);
						fileLen -= 16;
					}else {
						outputStream.write(dataToWrite, 0, (int)fileLen);
					}
				}
				inputStream.close();
				outputStream.close();
				System.out.print("decrypt end...\n");
			}catch(IOException e) {
				e.printStackTrace();
			}
		}else if(this.mode.equals("OFB")) {
			//decryption--------------------------
			System.out.print("(mode OFB)decrypt start...\n");
			File fileInput = new File(inputFilePath);
			File fileOutput = new File(outputFilePath);
			int count = 0;
			byte[] buffer = new byte[16];
			try {
				InputStream inputStream = new FileInputStream(fileInput);
				byte[] lengthSign = new byte[8];
				inputStream.read(lengthSign);
				long fileLen = 0;
				for(int i = 0; i < 8; i++) {
					fileLen += (long)(lengthSign[i] << (i*8));
				}
				OutputStream outputStream = new FileOutputStream(fileOutput);
				boolean isFirst16byte = true;
				while(-1!=(count=inputStream.read(buffer))) {
					if(isFirst16byte) {
						setDataMatrix(this.IV);
						isFirst16byte = false;
					}else {
						setDataMatrix(this.encryptionMat);
					}
					
					encrypt();
					setEncryptionMatrix();
					XORPlainText(buffer);
					
					byte[] dataToWrite = getDataMatrix();
					if(fileLen >= 16) {
						outputStream.write(dataToWrite);
						fileLen -= 16;
					}else {
						outputStream.write(dataToWrite, 0, (int)fileLen);
					}
				}
				inputStream.close();
				outputStream.close();
				System.out.print("decrypt end...\n");
			}catch(IOException e) {
				e.printStackTrace();
			}
		}else if(this.mode.equals("PCBC")) {
			//decryption--------------------------
			System.out.print("(mode PCBC)decrypt start...\n");
			File fileInput = new File(inputFilePath);
			File fileOutput = new File(outputFilePath);
			int count = 0;
			byte[] buffer = new byte[16];
			try {
				InputStream inputStream = new FileInputStream(fileInput);
				byte[] lengthSign = new byte[8];
				inputStream.read(lengthSign);
				long fileLen = 0;
				for(int i = 0; i < 8; i++) {
					fileLen += (long)(lengthSign[i] << (i*8));
				}
				OutputStream outputStream = new FileOutputStream(fileOutput);
				boolean isFirst16byte = true;
				while(-1!=(count=inputStream.read(buffer))) {
					setDataMatrix(buffer);
					decrypt();
					if(isFirst16byte) {
						XORIV();
						isFirst16byte = false;
					}else {
						XOREncryptionMat();
					}
					byte[] dataToWrite = getDataMatrix();
					if(fileLen >= 16) {
						outputStream.write(dataToWrite);
						fileLen -= 16;
					}else {
						outputStream.write(dataToWrite, 0, (int)fileLen);
					}
					XORPlainText(buffer);
					setEncryptionMatrix();
				}
				inputStream.close();
				outputStream.close();
				System.out.print("decrypt end...\n");
			}catch(IOException e) {
				e.printStackTrace();
			}
		}else if(this.mode.equals("CTR")) {
			//decryption--------------------------
			System.out.print("(mode CTR)decrypt start...\n");
			File fileInput = new File(inputFilePath);
			File fileOutput = new File(outputFilePath);
			int count = 0;
			byte[] buffer = new byte[16];
			try {
				InputStream inputStream = new FileInputStream(fileInput);
				byte[] lengthSign = new byte[8];
				inputStream.read(lengthSign);
				long fileLen = 0;
				for(int i = 0; i < 8; i++) {
					fileLen += (long)(lengthSign[i] << (i*8));
				}
				OutputStream outputStream = new FileOutputStream(fileOutput);
				long counterSuffix = 0;
				while(-1!=(count=inputStream.read(buffer))) {
					byte[] counter = getCounter(counterSuffix);
					setDataMatrix(counter);
					encrypt();
					XORPlainText(buffer);
					counterSuffix++;
					byte[] dataToWrite = getDataMatrix();
					if(fileLen >= 16) {
						outputStream.write(dataToWrite);
						fileLen -= 16;
					}else {
						outputStream.write(dataToWrite, 0, (int)fileLen);
					}
				}
				inputStream.close();
				outputStream.close();
				System.out.print("decrypt end...\n");
			}catch(IOException e) {
				e.printStackTrace();
			}
		}
		
	}

	/**
	 * @author baiwenxin
	 * @param str, string to be decrypted
	 * @return String, decrypted string
	 * @brief decrypt string
	 */
	public String decryptString(String str) {
		String ret = "";
		return ret;
	}

}
